/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package digitaldreamers.libraries.games.clients.networkmanagement.concrete;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import digitaldreamers.libraries.games.clients.networkmanagement.abstracts.IConnectionHandler;
import digitaldreamers.libraries.games.clients.networkmanagement.abstracts.IConnectionHandler.OnDataReceivedListener;
import digitaldreamers.libraries.games.clients.networkmanagement.packets.sending.AbSendablePacket;

/**
 *
 * @author DaoLe
 */
public class ConnectionHandler implements IConnectionHandler, Runnable {
    
    private static final String INVALID_PACKET_LENGTH_ERROR_MESSAGE = "Packet length must be greater than 0 and less than 1025.";
    private static final String DATA_READING_ERROR_MESSAGE = "Could not read packet data.";
    
    private static final String DEFAULT_GAME_SERVER_IP = "192.168.0.100";
    private static final int DEFAULT_GAME_SERVER_PORT = 7777;
    private static final int SLEEP_TIME = 100;
    private static final int PACKET_LENGTH_BYTES = 4;
    private static final int MAX_PACKET_LENGTH_BYTES = 1024;
    
    private String m_strIP;
    private int m_iPort;
    private volatile boolean m_bIsRunning;
    private boolean m_bIsConnected;
    private Socket m_Socket;
    private Thread m_DataReceivingThread;
    private DataOutputStream m_DataOutputStream;
    private InputStream m_InputStream;
    private ArrayList<OnDataReceivedListener> m_ListenerList;
    
    public boolean isRunning() {
        return m_bIsRunning;
    }
    
    public boolean isConnected() {
        return m_bIsConnected;
    }
    
    public ConnectionHandler() {
        m_strIP = DEFAULT_GAME_SERVER_IP;
        m_iPort = DEFAULT_GAME_SERVER_PORT;
        m_bIsRunning = false;
        m_bIsConnected = false;
        m_Socket = null;
        m_DataReceivingThread = null;
        m_DataOutputStream = null;
        m_InputStream = null;
        m_ListenerList = new ArrayList<>();
    }
    
    @Override
    public void connectToServer(String strIP, int iPort) throws UnknownHostException, IOException {
        if (m_bIsConnected) {
            return;
        }

        if(strIP != null && !"".equals(strIP)) {
            m_strIP = strIP;
        }
        
        if(iPort > 0) {
            m_iPort = iPort;
        }
        
        m_Socket = new Socket(m_strIP, m_iPort);
        m_InputStream = m_Socket.getInputStream();
        m_DataOutputStream = new DataOutputStream(new BufferedOutputStream(m_Socket.getOutputStream()));
        m_DataReceivingThread = new Thread(this);
        m_DataReceivingThread.start();
        m_bIsRunning = true;
        m_bIsConnected = true;
    }

    @Override
    public void addEventListener(OnDataReceivedListener listener) {
        m_ListenerList.add(listener);
    }

    @Override
    public void removeEventListener(OnDataReceivedListener listener) {
        m_ListenerList.remove(listener);
    }

    @Override
    public void fireDataReceivedEvent(byte[] arr_byData) throws IOException {
        for (OnDataReceivedListener listener : m_ListenerList) {
            listener.onDataReceived(arr_byData);
        }
    }

    @Override
    public void run() {
        while (m_bIsRunning) {
            try {
                if (m_Socket.getInputStream().available() > 0) {
                    byte[] data = receiveFixedData(m_InputStream, PACKET_LENGTH_BYTES);
                    if (data == null) {
                        continue;
                    }

                    //get message length
                    ByteArrayInputStream BAIS = new ByteArrayInputStream(data);
                    DataInputStream DIS = new DataInputStream(BAIS);

                    int messageLength = DIS.readInt();
                    if (messageLength > 0) {
                        data = receiveFixedData(m_InputStream, messageLength);
                        if (data == null) {
                            continue;
                        }
                    }
                    fireDataReceivedEvent(data);
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
            }
            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException ex) {
            }
        }
    }

    private byte[] receiveFixedData(InputStream inputStream, int iCount) throws Exception {
        // Check the data.
        if (iCount <= 0 || iCount > MAX_PACKET_LENGTH_BYTES) {
            throw new IllegalArgumentException(INVALID_PACKET_LENGTH_ERROR_MESSAGE);
        }

        // Set the buffer with the input length.
        byte[] arr_byBuffer = new byte[iCount];
        int iReceivedCount;
        int iPosition = 0;
        
        // Read until all data is received.
        do {
            // Read the stream to buffer.
            iReceivedCount = inputStream.read(arr_byBuffer, iPosition, iCount - iPosition);
            if (iReceivedCount == 0) {
                throw new Exception(DATA_READING_ERROR_MESSAGE);
            }
            iPosition += iReceivedCount;
        } while (iPosition < iCount);
        return arr_byBuffer;
    }

    @Override
    public void send(AbSendablePacket sendablePacket) throws IOException {
        byte[] arr_byData = sendablePacket.toByteArray();
        
        // Send the length of the data first.
        m_DataOutputStream.write(convertIntToByteArray(arr_byData.length));
        m_DataOutputStream.flush();
        
        // Then send the real data.
        m_DataOutputStream.write(arr_byData);
        m_DataOutputStream.flush();
    }

    private byte[] convertIntToByteArray(int iValue) {
        return new byte[] {
            (byte) (iValue >>> 24),
            (byte) (iValue >>> 16),
            (byte) (iValue >>> 8),
            (byte) iValue
        };
    }

    public void close() {
        m_bIsRunning = false;
        try {
            m_DataReceivingThread.join();
            m_Socket.close();
        } catch (Exception e) {
        }
    }
}
